module Boids.Rules

open Boids;;
open Boids.Utilities;;
open System;;
open Microsoft.Xna.Framework;;


let boid_check_x_bounds (settings:BoidSettings) (b:Boid) =
  match b with
  | _ when b.Position.X < settings.wall ->
      new Vector2(settings.wall_force, 0.f);
      
  | _ when b.Position.X > (settings.width - settings.wall) ->
      new Vector2(-settings.wall_force, 0.f);

  | _ -> new Vector2();;


let boid_check_y_bounds (settings:BoidSettings) (b:Boid) =
  match b with
  | _ when b.Position.Y < settings.wall ->
      new Vector2(0.f, settings.wall_force);

  | _ when b.Position.Y > (settings.height - settings.wall) ->
      new Vector2(0.f, -settings.wall_force);

  | _ -> new Vector2();;
    
    
let boid_steer (b:Boid) v (settings:BoidSettings) slowdown = 
  let l = settings.steer_force in
  let dir = v - b.Position in

  let sm d =
    match d with
      | _ when slowdown && d < settings.range ->
         Vector2.Clamp(((dir * (settings.max_vel * (d / 100.0f))) - b.LinearVelocity), (new Vector2(-l, -l)), (new Vector2(l, l)));
        
      | _ ->
        Vector2.Clamp(((dir * settings.max_vel) - b.LinearVelocity), (new Vector2(-l, -l)), (new Vector2(l, l))) in
  
  match dir.Length() with
    | _ as d when d > 0.f ->
      dir.Normalize();
      sm d;
           
    | _ -> (new Vector2(0.f, 0.f));;


let boid_adjust_rotation (b:Boid) = 
  b.Rotation <- heading(b.LinearVelocity);
  b;;


let boid_neighbors (b:Boid) (bl:(Boid list)) r = 
  let sort = List.sort(fun ((d1:single), _) ((d2:single), _) -> d1.CompareTo(d2)) in

    let (nl, nnl) = [for n in bl -> (Vector2.Distance(b.Position,  n.Position), n)] |> 
        List.partition(fun (d, _) -> d < r) |> 
          fun (a, b) -> (a |> sort, b |> sort) in
    
    let count = List.length nl in   
    match nl with
    | _ :: tl -> (tl, nnl);
    | _ -> ([], nnl);;
 
  
let boid_limit_vel (settings:BoidSettings) (b:Boid) = 
  let v = b.LinearVelocity.Length() in
  match v with
  | _ when v > settings.max_vel ->
        let vl = v / settings.max_vel in
        let force = b.LinearVelocity / vl in
        force;

  | _ -> new Vector2();;


let boid_cohesion b_sum count (settings:BoidSettings) =
  match count with
  | _ when count > 0.f -> 
    fun (b:Boid) ->
      let v = (b_sum.pos - b.Position) / count in
      let force = (v - b.Position) * settings.cohesion_mult in
      force;
      
  | _ -> 
    fun b -> new Vector2();;  


let boid_scatter b_sum count (settings:BoidSettings) =
  match rand.Next(20) with
  | _ as x when x % 2 = 0 -> 
    fun (b:Boid) ->
      let force = settings.scatter_mult * ( (boid_cohesion b_sum count settings)(b) ) in
      force;
      
  | _ -> 
    fun b -> new Vector2();; 


let boid_separation nl (settings:BoidSettings) (b:Boid) = 
  match List.length(nl) with
    | _ as x when x > 0 ->
      let list_sum = List.fold_left(
       
                                      fun (acc_vect:Vector2) (dist:single, n:Boid) ->
                                        let diff = b.Position - n.Position in
                                        acc_vect + (Vector2.Normalize(diff) / dist); 
                                        
                                    ) (new Vector2()) nl in

      let force = (list_sum / Convert.ToSingle(x)) * settings.separation_mult in
      force;
      
    | _ -> new Vector2();;
	      

let boid_align b_sum count (settings:BoidSettings) =
  match count with
  | _ when count > 0.f -> 
    fun (b:Boid) ->
      let force = (vector_limit (b_sum.lin_vel / count) settings.max_vel) * settings.align_mult in
      force;
      
  | _ -> 
    fun b -> new Vector2();;
        
      
let boid_wander (settings:BoidSettings) (b:Boid) =
  let wrad = settings.wander_settings.wander_radius in
  let wchange = settings.wander_settings.change in
  
  b.wander_theta <- b.wander_theta + rand_single_range(wchange);
  let act_theta = b.wander_theta + heading(b.LinearVelocity) in
  
  let r_c_loc = ( Vector2.Normalize(b.LinearVelocity) * settings.wander_settings.wander_dist ) + b.Position in        
  let c_offset = new Vector2(wrad * cos(act_theta), wrad * sin(act_theta)) in
  let target = r_c_loc + c_offset in
  let force = boid_steer b target settings false in
  force;;


let boid_apply_rules (bl:(Boid list)) (settings:BoidSettings) (b:Boid) = 
  let count = Convert.ToSingle(bl |> List.length) in
  let b_sum = boid_sum_list bl in

  let (nl, nnl) = boid_neighbors b bl settings.range in
  let ncount = Convert.ToSingle(nl |> List.length) in
  let nncount = Convert.ToSingle(nnl |> List.length) in
  
  let (_, nbl) = nl |> List.unzip in
  let nb_sum = boid_sum_list nbl in
  
  let (_, nnbl) = nnl |> List.unzip in
  let nnb_sum = boid_sum_list nnbl in
  
  let v1 = b |> boid_check_y_bounds settings in
  let v2 = b |> boid_check_x_bounds settings in
  let v3 = b |> boid_cohesion nnb_sum nncount settings in
  let v4 = b |> boid_separation nl settings in
	let v5 = b |> boid_align nb_sum ncount settings in      
  let v6 = b |> boid_wander settings in
  let v7 = b |> boid_scatter nb_sum ncount settings in
  let vs = v1 + v2 + v3 + v4 + v5 + v6 + v7 in
  //let v = boid_steer b vs settings false in
  b.ApplyForce(vs);
  b |> boid_adjust_rotation;;


let boid_list_apply_rules bl settings = 
  let b_func = boid_apply_rules bl settings in
  [for b in bl -> async { return b_func(b) }] |> Async.Parallel |> Async.Run |> Array.to_list